RAPHAËL Private Dining
«Schnell, unkompliziert und genau so, wie ich es mir vorgestellt habe. Die Seite sieht auf dem Handy genauso gut aus wie am Computer.»
Ich baue Websites für Läden, Werkstätten und Restaurants in der ganzen Schweiz. Die erste Demo kostet Sie nichts.
Bis auf RAPHAËL Private Dining sind alle Projekte unabhängige Konzepte ohne Auftrag. Die gezeigten Betriebe sind nicht meine Kunden.
«Schnell, unkompliziert und genau so, wie ich es mir vorgestellt habe. Die Seite sieht auf dem Handy genauso gut aus wie am Computer.»




Drei Pakete mit Fixpreis, damit Sie vorher wissen, was es kostet. Nicht sicher, welches? Schreiben Sie mir, was Sie brauchen, ich schlage das passende vor.
Eine Seite mit dem Wichtigsten. Reicht den meisten kleinen Betrieben.
Mehrere Seiten für Betriebe mit grösserem Angebot.
Für Betriebe, die aus ihrer Website mehr machen wollen als eine Visitenkarte.
Das erste Jahr ist in jedem Paket inbegriffen. Danach freiwillig und jährlich kündbar. Ihre Seite läuft auch ohne Betreuung weiter, sie gehört Ihnen.
Wenn Sie sich innert einer Woche nach der Demo entscheiden.
Drei Wege zu einer neuen Website im Überblick.
Ein allgemeiner Vergleich, im Einzelfall kann es anders aussehen.
Sie sehen eine erste Demo, bevor Sie etwas zahlen. Bezahlt wird in zwei Hälften, die erste, wenn Sie Ja sagen, die zweite bei der Übergabe.
Ich schaue mir Ihre Website und Ihr Google-Profil an: Handy-Ansicht, Öffnungszeiten, Links, Impressum, Ladezeit.
1–2 TageIch baue eine erste Version Ihrer neuen Seite mit Ihren Texten und Bildern und zeige sie Ihnen, im Geschäft oder per Video-Call.
3–5 TageGefällt Ihnen die Richtung, unterschreiben wir eine kurze Vereinbarung. Sobald die Anzahlung da ist, geht es los.
50 % AnzahlungIch baue die Seite fertig aus und zeige sie Ihnen nochmals. Was Sie noch ändern möchten, passe ich an.
1–3 Wochen, je nach PaketIch verbinde das Kontaktformular mit Ihrer E-Mail, schalte die Seite auf Ihrer Domain auf und übergebe Ihnen alles.
50 % bei Übergabe
Ich bin Marcus Miller, gelernter Koch. Heute mache ich Websites für Geschäfte in der ganzen Schweiz, meistens für Leute, die ihren Laden selber führen und weder Zeit noch Lust haben, sich mit sowas herumzuschlagen.
Ich baue jede Seite von Hand, ohne Baukasten und ohne Plugins, denn die funktionieren nach ein paar Jahren oft nicht mehr. Ihre Seite lädt schnell, läuft auf einem Konto auf Ihren Namen, und Sie zahlen kein Baukasten-Abo.
Ihre Frage ist nicht dabei? Schreiben Sie mir, ich antworte innert 24 bis 48 Stunden.
Frage stellenBasis kostet CHF 1'900, Ausbau CHF 3'400 und Vollausbau CHF 4'900, als Fixpreis mit dem ersten Jahr Betreuung. Extras wie ein Fotoshooting oder eine weitere Sprache kommen dazu, und Sie wissen es vorher. Ab dem zweiten Jahr können Sie die Betreuung für CHF 849 pro Jahr weiterführen, müssen aber nicht.
Nein. Die erste Demo kostet Sie nichts. Erst wenn sie Ihnen gefällt und Sie Ja sagen, zahlen Sie 50 % an, den Rest bei der Übergabe.
Dann sagen Sie mir, was nicht passt. Wenn wir keine Richtung finden, die Ihnen gefällt, lassen wir es, und Sie schulden mir nichts.
Die erste Demo steht in 3 bis 5 Tagen. Nach Ihrer Zusage ist die Seite je nach Paket in 1 bis 3 Wochen fertig. Von der ersten Anfrage bis zur fertigen Seite sind es meist 2 bis 4 Wochen.
Ja. Die Seite läuft auf einem Hosting-Konto auf Ihren Namen, und Sie bekommen bei der Übergabe alles. Sie läuft auch weiter, wenn Sie die Betreuung nicht verlängern.
Ja. Im ersten Jahr passe ich Öffnungszeiten, Preise, Texte oder Bilder für Sie an, das ist inbegriffen. Grössere Umbauten, etwa eine neue Seite oder eine neue Funktion, besprechen wir vorher und ich nenne Ihnen den Preis. Ab dem zweiten Jahr läuft das über die Betreuung.
In der ganzen Schweiz. Ich bin in Zürich zuhause, die Demo zeige ich Ihnen bei Ihnen im Geschäft oder per Video-Call.
Jede Seite ist für Google eingerichtet, mit Titel und Beschreibung, schneller Ladezeit und der Verknüpfung mit Ihrem Google-Unternehmensprofil.
Schicken Sie mir den Namen Ihres Geschäfts, dann schaue ich mir Ihre Website und Ihr Google-Profil an und melde mich innert 24 bis 48 Stunden mit einem Vorschlag. Das kostet Sie nichts.
Lieber per E-Mail? kontakt@marcusmiller.ch
${tt.f}
${indepAll()}
${o.p}
${o.care.p}
${o.bonus.p}
${o.cmp.p}
${o.cmp.note}
${p.p}
${s.p}
${s.pay?`${s.pay}`:s.d}${a.p1}
${a.p2}
${q[1]}

${c.sector[LANG]}
${w.more} ${arrowR}${libText(cat)}
${indepAll()}
${q.text[LANG]}
${L.nf}
${L.live} ↗${L.wait}
${L.live} ↗`; f.classList.add('wait'); let loaded = false; f.addEventListener('load', ()=>{ loaded = true; f.classList.remove('wait'); }, {once:true}); setTimeout(()=>{ if(!loaded && pl.isConnected){ pl.querySelector('p').textContent = L.slowOld; const sp = pl.querySelector('.spin'); if(sp) sp.remove(); } }, 12000); f.setAttribute('sandbox','allow-popups allow-popups-to-escape-sandbox'); f.src = src.slice(4); } else if(src.startsWith('demo:')){ const dc = CASES.find(x=>x.id===src.slice(5)); if(dc && dc.demoUrl && /^https?:$/.test(location.protocol)){ f.src = dc.demoUrl; } else loadBlock('demo', src.slice(5)).then(html=>{ if(html) f.srcdoc = embedHtml(html,'about:srcdoc'); }); } else if(src.startsWith('old:')){ f.title='Alte Website'; loadBlock('old', src.slice(4)).then(html=>{ if(html) f.srcdoc = embedHtml(html,'about:srcdoc'); }); } guardOn = true; const m = body.querySelector('.mk'); if(m) m.remove(); f.addEventListener('load', ()=>{ try{ const r=body.getBoundingClientRect(), far = r.bottom < -innerHeight*0.6 || r.top > innerHeight*1.6; const d=f.contentDocument; if(d&&d.documentElement) d.documentElement.classList.toggle('mm-paused', far); }catch(_){} }); body.insertBefore(f, body.firstChild); if(pl) body.insertBefore(pl, f); // CSS zoom (Chrome/Safari/Firefox 126+) rasterises at the shown size and is much cheaper to scroll than transform:scale; transform stays as fallback const ZOOM = CSS.supports('zoom','0.5'); const fit = ()=>{ const w = body.clientWidth; if(!w) return; if(body.classList.contains('native')){ f.style.width='100%'; f.style.transform='none'; f.style.zoom=''; f.style.height='100%'; return; } const base = matchMedia('(max-width:640px)').matches?430:1280, s = w/base; f.style.width = base+'px'; f.style.height = (body.clientHeight/s)+'px'; if(ZOOM){ f.style.zoom = s; f.style.transform='none'; } else { f.style.transform = `scale(${s})`; } }; fit(); new ResizeObserver(fit).observe(body); body.addEventListener('refit', fit); } // rogue-scroll guard: embedded documents can scroll this page (fragment navigation, focus). If the page scrolls with no // user input and it wasn't us, put it back. if('scrollRestoration' in history) history.scrollRestoration = 'manual'; let lastInput = 0, ownUntil = performance.now() + 300, goodY = scrollY, guardOn = false; const GUARD_OFF = matchMedia('(pointer:coarse)').matches; // on touch screens the guard fought fast flicks ['wheel','touchstart','touchmove','keydown','pointerdown'].forEach(ev=>addEventListener(ev, ()=>{ lastInput = performance.now(); }, {passive:true, capture:true})); const _sto = window.scrollTo.bind(window); window.scrollTo = function(a){ const smooth = a && typeof a==='object' && a.behavior==='smooth'; ownUntil = Math.max(ownUntil, performance.now() + (smooth ? 1500 : 150)); return _sto.apply(window, arguments); }; addEventListener('hashchange', ()=>{ ownUntil = Math.max(ownUntil, performance.now() + 1500); }); const _siv = Element.prototype.scrollIntoView; Element.prototype.scrollIntoView = function(){ ownUntil = Math.max(ownUntil, performance.now() + 1500); return _siv.apply(this, arguments); }; addEventListener('scroll', ()=>{ const now = performance.now(); if(guardOn && !GUARD_OFF && now - lastInput > 600 && now > ownUntil && Math.abs(scrollY - goodY) > 120 && document.body.style.overflow!=='hidden'){ ownUntil = now + 150; _sto({top:goodY, behavior:'instant'}); return; } goodY = scrollY; },{passive:true}); // idle pre-mount: one pane every ~900ms, only when the user has not scrolled for 400ms and the tab is visible let lastScrollAt = 0; addEventListener('scroll', ()=>{ lastScrollAt = performance.now(); }, {passive:true}); const farIO = new IntersectionObserver(es=>es.forEach(e=>{ e.target.querySelectorAll('iframe').forEach(f=>{ try{ const d=f.contentDocument; if(d&&d.documentElement) d.documentElement.classList.toggle('mm-paused', !e.isIntersecting); }catch(_){} }); }),{rootMargin:'60% 0px 60% 0px'}); const SLOW = (()=>{ const c = navigator.connection; return !!(c && (c.saveData || /(^|-)2g$|3g/.test(c.effectiveType||''))); })(); function premountTick(){ if(SLOW) return; if(document.hidden || performance.now()-lastScrollAt < 250) return setTimeout(premountTick, 200); const phone = matchMedia('(max-width:640px)').matches; const next = [...document.querySelectorAll('.pane-body')].find(b=>b._mount && !b._done && (!phone || b.offsetParent)); if(next){ next._mount(); setTimeout(premountTick, 650); } else setTimeout(premountTick, 2000); } setTimeout(premountTick, 350); function syncPanes(){ const phone = matchMedia('(max-width:640px)').matches; document.querySelectorAll('[data-cmp]').forEach(el=>{ const tabs = el.previousElementSibling && el.previousElementSibling.hasAttribute('data-tabs') ? el.previousElementSibling : (el.closest('[data-vs]')||document).querySelector('[data-tabs]'); const on = tabs.querySelector('button.on').dataset.p; el.querySelectorAll('[data-pane]').forEach(pn=>pn.classList.toggle('hide', phone && pn.dataset.pane!==on)); }); } addEventListener('resize', syncPanes); function updateReach(){ const el = document.getElementById('reach'); if(!el) return; const a = t().about, H = CONTACT.hours; const p = new Intl.DateTimeFormat('en-GB',{timeZone:'Europe/Zurich',weekday:'short',hour:'numeric',hour12:false}).formatToParts(new Date()); const wd = {Sun:0,Mon:1,Tue:2,Wed:3,Thu:4,Fri:5,Sat:6}[p.find(x=>x.type==='weekday').value]; const hr = +p.find(x=>x.type==='hour').value; el.classList.add('on'); el.querySelector('span').textContent = a.reply; } setInterval(updateReach, 60000); /* ---------- v68: spam protection for the contact form ---------- */ const PAGE_T0 = Date.now(); function looksLikeBot(f){ if(Date.now() - PAGE_T0 < 3000) return true; // nobody fills the form within 3 s of opening the page if(!f._human) return true; // submitted by a script, not by a click or key press const msg = (f.querySelector('[name=nachricht]') || {}).value || ''; if((msg.match(/https?:\/\/|www\./gi) || []).length >= 3) return true; // link spam return false; } function fakeOk(f){ const k = t().contact.f, v = (f.querySelector('[name=vorname]') || {}).value || ''; const ok = f.querySelector('[data-ok]'); if(ok) ok.textContent = k.ok.replace('{name}', v); f.classList.add('sent'); } const SEND_KEY = 'mm-sends', SEND_MAX = 3, SEND_WINDOW = 60*60*1000; function sendLog(){ try{ return JSON.parse(localStorage.getItem(SEND_KEY) || '[]').filter(x=>Date.now()-x < SEND_WINDOW); }catch(e){ return []; } } function sendAllowed(){ return sendLog().length < SEND_MAX; } function countSend(){ try{ const l = sendLog(); l.push(Date.now()); localStorage.setItem(SEND_KEY, JSON.stringify(l)); }catch(e){} } function limitNote(f){ let n = f.querySelector('.limit-note'); if(!n){ n = document.createElement('p'); n.className = 'limit-note'; n.setAttribute('role','status'); const b = f.querySelector('button[type=submit]'); b.insertAdjacentElement('afterend', n); } n.innerHTML = LANG==='en' ? `You've already sent me a few messages in the last hour, thank you. I'll get back to you soon. Anything else? Write to ${CONTACT.email}.` : `Sie haben mir in der letzten Stunde schon ein paar Nachrichten geschickt, danke. Ich melde mich bald. Noch etwas? Schreiben Sie an ${CONTACT.email}.`; } function initForm(f){ const sel = f.querySelector('select[name=geschaeft]'), other = f.querySelector('label.other'); sel.addEventListener('change', ()=>{ const isOther = /Andere|Other/.test(sel.value); other.hidden = !isOther; other.querySelector('input').required = isOther; }); const tel = f.querySelector('input[name=telefon]'), telT = f.querySelector('.tel-t'), prefNote = f.querySelector('.pref-note'); f.querySelectorAll('input[name=kontakt]').forEach(r=>r.addEventListener('change', ()=>{ const needTel = r.checked && r.value !== t().contact.f.prefs[0]; tel.required = needTel; telT.textContent = needTel ? t().contact.f.phoneReq : t().contact.f.phone; prefNote.hidden = !needTel; })); // extras sit in an Ausbau and a Vollausbau box; the chosen package's box is outlined, Basis gets a note, an extra above the package moves it up const pk = f.querySelector('select[name=paket]'), ex = f.querySelector('.extras'), exNote = ex.querySelector('.ex-note'); const pkRank = v => { const o = t().offer; return v===o.t3.n ? 3 : v===o.t2.n ? 2 : v===o.t1.n ? 1 : 0; }; const pkName = n => n===3 ? t().offer.t3.n : t().offer.t2.n; function syncExtras(msg){ const r = pkRank(pk.value); ex.querySelectorAll('.xbox').forEach(b=>b.classList.toggle('on', r >= +b.dataset.tier)); const kf = t().contact.f; if(msg){ exNote.textContent = msg; exNote.classList.add('bump'); exNote.hidden = false; } else { exNote.classList.remove('bump'); exNote.textContent = r===1 ? kf.exLaunch : kf.exHint; exNote.hidden = r > 1; } } pk.addEventListener('change', ()=>{ const r = pkRank(pk.value), gone = []; if(r) ex.querySelectorAll('input:checked').forEach(c=>{ if(+c.closest('label').dataset.need > r){ c.checked = false; gone.push(c.value); } }); syncExtras(gone.length ? t().contact.f.exDrop.replace('{x}', gone.map(x=>'«'+x+'»').join(', ')).replace('{p}', pk.value) : ''); }); ex.querySelectorAll('input').forEach(c=>c.addEventListener('change', ()=>{ const need = +c.closest('label').dataset.need, r = pkRank(pk.value); if(c.checked && r < need){ pk.value = pkName(need); pk.classList.remove('flash'); void pk.offsetWidth; pk.classList.add('flash'); syncExtras(t().contact.f.exBump.replace('{p}', pkName(need)).replace('{x}', c.value)); } else syncExtras(); })); syncExtras(); ['pointerdown','keydown','touchstart'].forEach(ev=>f.addEventListener(ev, e=>{ if(e.isTrusted) f._human = true; }, {capture:true, passive:true})); f.addEventListener('submit', async e=>{ e.preventDefault(); if(f.querySelector('input[name=bot-field]').value){ f.classList.add('sent'); return; } if(looksLikeBot(f)){ fakeOk(f); return; } if(!f.reportValidity()) return; if(f.classList.contains('busy')) return; if(!sendAllowed()){ limitNote(f); return; } const btn = f.querySelector('button[type=submit]'); btn.disabled = true; const kf = t().contact.f, label = btn.textContent; f.classList.add('busy'); btn.classList.add('sending'); btn.innerHTML = '' + kf.sending; const slowNote = f.querySelector('.slow'); const slowT = setTimeout(()=>{ if(slowNote) slowNote.hidden = false; }, 5000); const ctrl = new AbortController(); const killT = setTimeout(()=>ctrl.abort(), 40000); const done = ()=>{ clearTimeout(slowT); clearTimeout(killT); if(slowNote) slowNote.hidden = true; f.classList.remove('busy'); btn.classList.remove('sending'); btn.textContent = label; }; const fd = new FormData(f); const wishes = [fd.get('start'), ...fd.getAll('wunsch')].filter(Boolean); const lines = [ ['Name', fd.get('vorname')+' '+fd.get('nachname')], ['E-Mail', fd.get('email')], ['Telefon', fd.get('telefon') || '-'], ['Kontakt per', fd.get('kontakt') || '-'], ['Geschäft', fd.get('geschaeft') + (fd.get('geschaeft_andere') ? ' ('+fd.get('geschaeft_andere')+')' : '')], ['Website', fd.get('website') || '-'], ['Paket', fd.get('paket') || '-'], ['Wunsch', wishes.length ? wishes.join(', ') : '-'], ['Nachricht', fd.get('nachricht') || '-'] ]; const subject = 'Website-Anfrage: ' + (fd.get('geschaeft') || '') + ' / ' + fd.get('vorname') + ' ' + fd.get('nachname'); const body = lines.map(l=>l[0]+': '+l[1]).join('\n'); const hosted = /^https?:$/.test(location.protocol) && !/^(localhost|127\.)/.test(location.hostname); try{ if(CONTACT.emailjs.key){ const p = { name: fd.get('vorname')+' '+fd.get('nachname'), vorname: fd.get('vorname'), nachname: fd.get('nachname'), email: fd.get('email'), telefon: (fd.get('telefon') || '-') + ' · Kontakt per ' + (fd.get('kontakt') || 'E-Mail'), kontakt: fd.get('kontakt') || '-', geschaeft: fd.get('geschaeft') + (fd.get('geschaeft_andere') ? ' ('+fd.get('geschaeft_andere')+')' : ''), website: fd.get('website') || '-', wunsch: (fd.get('paket') ? 'Paket: '+fd.get('paket')+(wishes.length?' / ':'') : '') + (wishes.length ? wishes.join(', ') : (fd.get('paket') ? '' : '-')), paket: fd.get('paket') || '-', nachricht: fd.get('nachricht') || '-', subject, sprache: LANG }; const r = await fetch('https://api.emailjs.com/api/v1.0/email/send', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({service_id: CONTACT.emailjs.service, template_id: CONTACT.emailjs.template, user_id: CONTACT.emailjs.key, template_params: p}), signal: ctrl.signal}); if(!r.ok) throw 0; } else if(!CONTACT.formspree.includes('YOUR_ID')){ const r = await fetch(CONTACT.formspree,{method:'POST',headers:{Accept:'application/json'},body:fd}); if(!r.ok) throw 0; } else if(hosted){ const params = new URLSearchParams(); params.set('form-name','anfrage'); fd.forEach((v,k)=>{ if(k==='wunsch') return; params.set(k, v); }); params.set('wunsch', wishes.join(', ')); params.set('subject', subject); const r = await fetch(location.pathname, {method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body: params.toString()}); if(!r.ok) throw 0; } else { location.href = 'mailto:' + CONTACT.email + '?subject=' + encodeURIComponent(subject) + '&body=' + encodeURIComponent(body); } const k = t().contact.f, viaMail = !CONTACT.emailjs.key && CONTACT.formspree.includes('YOUR_ID') && !hosted; f.querySelector('[data-ok]').textContent = (viaMail ? k.okMail+' ' : '') + k.ok.replace('{name}', fd.get('vorname')); f.querySelector('[data-okto]').textContent = k.okTo + ' ' + fd.get('email'); done(); btn.disabled = true; countSend(); f.classList.add('sent'); f.scrollIntoView({block:'center', behavior:'smooth'}); }catch(_){ done(); btn.disabled = false; f.querySelector('.fail').hidden = false; location.href = 'mailto:' + CONTACT.email + '?subject=' + encodeURIComponent(subject) + '&body=' + encodeURIComponent(body); } }); } /* ---------- language ---------- */ function applyLang(){ document.documentElement.lang = LANG; document.querySelectorAll('[data-t]').forEach(el=>{ el.textContent = t()[el.dataset.t]; }); document.querySelectorAll('[data-tlabel]').forEach(el=>el.setAttribute('aria-label', t()[el.dataset.tlabel])); document.querySelectorAll('[data-lang]').forEach(b=>b.classList.toggle('on', b.dataset.lang===LANG)); currentView = ''; route(); } /* ---------- v67: switching the language keeps you where you are ---------- */ const LANG_ANCHOR = 'main section, main article, main h1, main h2, main h3, main p, main li, main details, main figure, main blockquote, main .pane, main .tier, main .step, main .br-tile'; function langSnapshot(){ const top = (document.getElementById('hd') || {offsetHeight:64}).offsetHeight || 64; const els = [...app.querySelectorAll(LANG_ANCHOR)]; let bi = -1, bt = Infinity, ci = -1, ch = Infinity; els.forEach((el,i)=>{ const r = el.getBoundingClientRect(); if(!r.height) return; if(r.top >= top - 2 && r.top < bt){ bt = r.top; bi = i; } // first block starting below the header else if(r.top < top - 2 && r.bottom > top && r.height < ch){ ch = r.height; ci = i; } // smallest block crossing the header line }); const i = (ci >= 0 && (bi < 0 || bt > innerHeight * .5)) ? ci : bi; const st = { n: els.length, i, off: i >= 0 ? els[i].getBoundingClientRect().top : 0, ratio: scrollY / Math.max(1, document.documentElement.scrollHeight - innerHeight), y: scrollY, faq: [...app.querySelectorAll('.faq details')].map(d=>d.open || d.classList.contains('is-open')), vsm: [...app.querySelectorAll('.vsm')].map(v=>v.dataset.vsm), form: [] }; const f = app.querySelector('#form'); if(f) [...f.elements].forEach(el=>{ if(!el.name || el.name==='bot-field' || el.type==='file' || el.type==='submit' || el.type==='button') return; if(el.type==='radio' || el.type==='checkbox'){ if(el.checked) st.form.push([el.name, [...f.querySelectorAll(`[name="${el.name}"]`)].indexOf(el), 'chk']); } else if(el.tagName==='SELECT'){ if(el.selectedIndex > 0) st.form.push([el.name, el.selectedIndex, 'sel']); } else if(el.value) st.form.push([el.name, el.value, 'txt']); }); return st; } function langRestore(st){ app.querySelectorAll('.faq details').forEach((d,i)=>{ if(st.faq[i]){ d.open = true; d.classList.add('is-open'); } }); app.querySelectorAll('.vsm').forEach((v,i)=>{ const k = st.vsm[i]; if(!k) return; v.dataset.vsm = k; v.querySelectorAll('[data-vsm-b]').forEach(b=>b.classList.toggle('on', b.dataset.vsmB===k)); }); const f = app.querySelector('#form'); if(f) st.form.forEach(([n,v,k])=>{ const el = k==='chk' ? f.querySelectorAll(`[name="${n}"]`)[v] : f.querySelector(`[name="${n}"]`); if(!el) return; if(k==='chk') el.checked = true; else if(k==='sel') el.selectedIndex = v; else el.value = v; el.dispatchEvent(new Event(k==='txt' ? 'input' : 'change', {bubbles:true})); }); const place = ()=>{ if(st.y <= 2){ scrollTo({top:0, behavior:'instant'}); return; } const els = [...app.querySelectorAll(LANG_ANCHOR)]; let y = (st.i >= 0 && els.length === st.n && els[st.i]) ? els[st.i].getBoundingClientRect().top + scrollY - st.off : st.ratio * (document.documentElement.scrollHeight - innerHeight); scrollTo({top: Math.max(0, Math.round(y)), behavior: 'instant'}); }; place(); // what is on screen (or above it) is shown at once: no fade-in, no counting up again const html = document.documentElement; html.classList.add('lang-snap'); app.querySelectorAll('.rv, .split, .hero-facts, .cs, .steps').forEach(el=>{ if(el.getBoundingClientRect().top < innerHeight){ el.classList.add('in'); el.removeAttribute('data-count'); el.querySelectorAll('[data-count]').forEach(c=>c.removeAttribute('data-count')); } }); void html.offsetHeight; requestAnimationFrame(()=>{ place(); requestAnimationFrame(()=>{ html.classList.remove('lang-snap'); }); }); let moved = false; const mark = ()=>{ moved = true; }; addEventListener('wheel', mark, {passive:true, once:true}); addEventListener('touchstart', mark, {passive:true, once:true}); addEventListener('keydown', mark, {once:true}); setTimeout(()=>{ if(!moved) place(); removeEventListener('wheel', mark); removeEventListener('touchstart', mark); removeEventListener('keydown', mark); }, 350); } function switchLang(l){ if(l === LANG) return; const st = langSnapshot(); LANG = l; try{ localStorage.setItem('mm-lang', LANG); }catch(e){} window._langKeep = true; try{ applyLang(); } finally { window._langKeep = false; } langRestore(st); } document.querySelectorAll('[data-lang]').forEach(b=>b.addEventListener('click',()=>{ switchLang(b.dataset.lang); })); /* ---------- links + phone menu bar ---------- */ document.addEventListener('click', e=>{ if(e.defaultPrevented || e.button!==0 || e.metaKey || e.ctrlKey || e.shiftKey) return; const a = e.target.closest && e.target.closest('a[href^="#/"]'); if(!a) return; if(a.getAttribute('href')===location.hash && (!PATHMODE || location.pathname==='/')){ e.preventDefault(); route(); } }); document.addEventListener('focusin', e=>{ if(e.target.matches && e.target.matches('input:not([type=checkbox]):not([type=radio]),textarea,select')) document.documentElement.classList.add('kb'); }); document.addEventListener('focusout', ()=>{ setTimeout(()=>{ const el = document.activeElement; if(!(el && el.matches && el.matches('input:not([type=checkbox]):not([type=radio]),textarea,select'))) document.documentElement.classList.remove('kb'); }, 80); }); /* ---------- header ---------- */ const burger = document.getElementById('burger'); burger.addEventListener('click',()=>{ const hd=document.getElementById('hd'); const o=hd.classList.toggle('open'); burger.classList.toggle('x',o); burger.setAttribute('aria-expanded',o); }); document.getElementById('yr').textContent = new Date().getFullYear(); /* ---------- go ---------- */ window._first = true; addEventListener('hashchange', route); applyLang();